package com.icw.decoupling.impl.module;

import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;

import com.icw.basis.BaseGeneric;
import com.icw.basis.BasePoEntity;
import com.icw.configuration.ConstSettings;
import com.icw.decoupling.defi.module.RestfulMgr;
import com.icw.decoupling.defi.proxy.GenericSrvProxy;
import com.icw.decoupling.defi.proxy.RestfulMgrProxy;
import com.icw.decoupling.defi.tool.DataExchange;
import com.icw.utility.AnnoHelper;
import com.icw.utility.BeanCtxUtils;
import com.icw.utility.IcwUtils;
import com.icw.utility.PoEntityHelper;
import com.icw.utility.excel.ExcelHeader;
import com.icw.utility.excel.ExcelPort;
import com.icw.utility.query.IcwCriteria;
import com.icw.utility.query.IcwCriterion;
import com.icw.utility.query.PageCount;

public abstract class RestfulMgrImpl<T extends BasePoEntity> extends BaseGeneric<T> implements RestfulMgr<T> {

	@Override
	public void getone(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		data.put("data", srv().one(getEntityClass(), (String) params.get("id")));
	}

	@Override
	public void getlist(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		PageCount page = de().parse(params.get("page"), PageCount.class);
		IcwCriteria cri = IcwUtils.ifNull(de().parse(params.get("criteria"), IcwCriteria.class), () -> new IcwCriteria());
		List<T> list = srv().query(getEntityClass(), cri, page);
		data.put("list", list);
		data.put("page", page);
	}

	@Override
	public void post(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		this.persist(params, request, response, data);
	}

	@Override
	public void patch(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		if (params.get("id") == null) {
			throw ConstSettings.notFound;
		} else {
			this.persist(params, request, response, data);
		}
	}

	protected void persist(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		Map<String, Object> optionals = new HashMap<>();
		for (String property : PoEntityHelper.keyOf(params)) {
			PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(getEntityClass(), property);
			// property does not exist, remove it.
			/*-*/if (pd == null) params.remove(property);
			// isUpdating and unUpdatable
			else if ("id".equals(property));
			// isUpdating and unUpdatable
			else if (!Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType()) && isUpdating(params) && !PoEntityHelper.isUpdatable(getEntityClass(), property)) params.remove(property);
			// isInserting and unInsertable
			else if (!Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType()) && isInserting(params) && !PoEntityHelper.isInsertable(getEntityClass(), property)) params.remove(property);
			// basic data type, which can be save directly, keep it and skip
			else if (PoEntityHelper.isBasicType(pd.getReadMethod().getReturnType()));
			// null value, which can be save directly, keep it and skip
			else if (params.get(property) == null);
			// detached value, which should be changed to id-only, and then do the rests
			else if (isDetachedToIdObjAndFalse(params, property));
			// persisted value, which can be save directly, keep it and skip
			else if (isPersisted(params, property));
			// required value, persist it
			else if (!PoEntityHelper.isNullable(getEntityClass(), property)) persistProperty(params, property);
			// optional value, cache and remove it
			else optionals.put(property, params.remove(property));
		}
		perisitRequiredParts(params);
		mergeUnrequiredParts((String) params.get("id"), optionals);
		data.put("data", srv().one(getEntityClass(), (String) params.get("id")));
	}

	@Override
	public void delete(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		T obj;
		if (params == null || params.get("id") == null) return;
		else if (null != (obj = srv().one(getEntityClass(), (String) params.get("id")))) {
			Set<Object> cascadables = new LinkedHashSet<>();
			Set<String> properties = PoEntityHelper.getUsefulProperties(getEntityClass());
			if (isLinkedListAndDetached(properties) || isMultiTreeAndDetached(properties)) {
				if (linkChildrenToParent(obj) > 0) obj = srv().one(getEntityClass(), obj.getId());
				properties.removeAll(SetUtils.unmodifiableSet("parent", "child", "children"));
			}
			Object value;
			for (String property : properties) {
				Class<?> fieldType = PoEntityHelper.findTypeByProperty(getEntityClass(), property);
				// basic field, skip
				/*-*/if (fieldType == null || PoEntityHelper.isBasicType(fieldType));
				// detached field, skip
				else if (!PoEntityHelper.isCascadable(getEntityClass(), property));
				// null value, skip
				else if ((value = PoEntityHelper.readValueByProperty(obj, property)) == null);
				// multiple field, delete directly
				else if (PoEntityHelper.isArry(getEntityClass(), property)) mgrDeleteMulti(obj, property, PoEntityHelper.toObjectArray(value));
				// required field, cache it
				else if (!PoEntityHelper.isNullable(getEntityClass(), property)) cascadables.add(value);
				// optional field, delete directly
				else mrgDeleteOne(obj, property, value);
			}
			srv().delete(getEntityClass(), obj);
			mgrDeleteMulti(cascadables.toArray());
		}
	}

	@Override
	public void exists(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		IcwCriteria cri = new IcwCriteria();
		Set<String> uniqueSet = PoEntityHelper.getUsefulProperties(getEntityClass()).stream()// e
				.filter(s -> AnnoHelper.isAnnoable(getEntityClass(), s, "unique"))// e
				.collect(Collectors.toSet());
		List<IcwCriterion> criList = Stream.of(PoEntityHelper.keyOf(params))// e
				.filter(key -> uniqueSet.contains(key))// e
				.map(k -> new IcwCriterion("eq", k, params.get(k)))// e
				.collect(Collectors.toList());
		criList.add(new IcwCriterion("ne", "id", params.get("id")));
		cri.criterions = criList.toArray(ArrayUtils.toArray());
		if (cri.criterions.length > 0) {
			data.put("exists", srv().countByCriteria(getEntityClass(), cri) > 0);
		} else {
			throw new Exception("No unique property offered.");
		}
	}

	// ---------------------------------------------------------------------------------------------------------------//
	private int linkChildrenToParent(T obj) throws Exception {
		// Update ${entityType} set parent.id = ${obj.parent.id} where parent.id = ${obj.id}
		return srv().updateByCriteria(getEntityClass(), IcwUtils.mapObject("parent.id", PoEntityHelper.readValueByProperty(obj, "parent.id")), new IcwCriteria("eq", "parent.id", obj.getId()));
	}

	private void mgrDeleteMulti(T obj, String property, Object[] values) throws Exception {
		RestfulMgrProxy mgr = BeanCtxUtils.getBeanByType(RestfulMgrProxy.class);
		@SuppressWarnings("unchecked")
		Class<? extends BasePoEntity> entityClass = (Class<? extends BasePoEntity>) PoEntityHelper.findTypeByProperty(getEntityClass(), property);
		for (Object temp : values) mgr.delete(entityClass, IcwUtils.idObj(temp), null, null, null);
	}

	@SuppressWarnings("unchecked")
	private void mgrDeleteMulti(Object[] array) throws Exception {
		for (Object value : array) {
			RestfulMgrProxy mgr = BeanCtxUtils.getBeanByType(RestfulMgrProxy.class);
			mgr.delete((Class<? extends BasePoEntity>) value.getClass(), IcwUtils.idObj(value), null, null, null);
		}
	}

	private void mrgDeleteOne(T obj, String property, Object value) throws Exception {
		srv().updateByCriteria(getEntityClass(), IcwUtils.mapObject(property, null), new IcwCriteria("eq", "id", obj.getId()));
		RestfulMgrProxy mgr = BeanCtxUtils.getBeanByType(RestfulMgrProxy.class);
		@SuppressWarnings("unchecked")
		Class<? extends BasePoEntity> entityClass = (Class<? extends BasePoEntity>) PoEntityHelper.findTypeByProperty(getEntityClass(), property);
		mgr.delete(entityClass, IcwUtils.idObj(value), null, null, null);
	}

	private void mergeUnrequiredParts(String id, Map<String, Object> paramsCache) throws Exception {
		if (paramsCache.size() == 0) {
			// skip
		} else {
			for (String property : new LinkedList<>(paramsCache.keySet())) {
				persistProperty(paramsCache, property);
			}
			updatePropertiesById(id, paramsCache);
		}
	}

	private void perisitRequiredParts(Map<String, Object> params) throws Exception {
		String id = (String) params.get("id");
		if (id == null) {
			id = srv().save(de().parse(params, getEntityClass())).getId();
		} else {
			updatePropertiesById(id, params);
		}
		params.clear();
		params.put("id", id);
	}

	private void updatePropertiesById(String id, Map<String, Object> patchs) throws Exception {
		if (isExistsMultiPart(patchs)) {
			srv().save(PoEntityHelper.objectAssign(srv().one(getEntityClass(), id), patchs));
		} else if (patchs.size() > 1 || (patchs.size() > 0 && !patchs.containsKey("id"))) {
			srv().updateByCriteria(getEntityClass(), patchs_2_contains_4_update(patchs), new IcwCriteria("eq", "id", id));
		}
	}

	private Map<String, Object> patchs_2_contains_4_update(Map<String, Object> patchs) {
		Map<String, Object> contains = IcwUtils.mapObject();
		for (Entry<String, Object> entry : patchs.entrySet()) {
			PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(getEntityClass(), entry.getKey());
			if (BasePoEntity.class.isAssignableFrom(pd.getReadMethod().getReturnType())) {
				contains.put(entry.getKey() + ".id", PoEntityHelper.readValueByProperty(entry.getValue(), "id"));
			} else {
				contains.put(entry.getKey(), entry.getValue());
			}
		}
		return contains;
	}

	private Object[] readValueAsArray(Object params, String property) {
		PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(getEntityClass(), property);
		Object[] values;
		if (Collection.class.isAssignableFrom(pd.getReadMethod().getReturnType())) {
			values = PoEntityHelper.toObjectArray(PoEntityHelper.readValueByProperty(params, property));
		} else {
			values = ArrayUtils.toArray(PoEntityHelper.readValueByProperty(params, property));
		}
		return values;
	}

	private void persistProperty(Map<String, Object> params, String property) throws Exception {
		PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(getEntityClass(), property);
		Class<?> fieldType = pd.getReadMethod().getReturnType();
		fieldType = Collection.class.isAssignableFrom(fieldType) ? PoEntityHelper.findComponentType(pd) : fieldType;
		Object[] values = readValueAsArray(params, property);
		for (int i = 0; i < values.length; i++) {
			Object value = values[i];
			if (!isPersisted(value)) {
				PoEntityHelper.fillParam(pd, value);
				persistValue(fieldType, Map.class.cast(value));
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void persistValue(Class<?> fieldType, Object value) throws Exception {
		Object res = IcwUtils.mgrSave((Class<? extends BasePoEntity>) fieldType, Map.class.cast(value));
		Map.class.cast(value).clear();
		Map.class.cast(value).put("id", PoEntityHelper.readValueByProperty(res, "id"));
	}

	private boolean isMultiTreeAndDetached(Set<String> properties) {
		return properties.containsAll(SetUtils.unmodifiableSet("parent", "children")) && !PoEntityHelper.isCascadable(getEntityClass(), "children");
	}

	private boolean isLinkedListAndDetached(Set<String> properties) {
		return properties.containsAll(SetUtils.unmodifiableSet("parent", "child")) && !PoEntityHelper.isCascadable(getEntityClass(), "child");
	}

	private boolean isDetachedToIdObjAndFalse(Map<String, Object> params, String property) {
		boolean cascadable = PoEntityHelper.isCascadable(getEntityClass(), property);
		Object value = PoEntityHelper.readValueByProperty(params, property);
		if (PoEntityHelper.isArry(getEntityClass(), property)) {
			Object[] objects = PoEntityHelper.toObjectArray(value);
			for (int i = 0; i < objects.length; i++) {
				boolean isInsert = PoEntityHelper.readValueByProperty(objects[i], "id") == null;
				if (!cascadable) {
					objects[i] = IcwUtils.idObj(objects[i]);
				} else if (isInsert && !PoEntityHelper.isInsertable(getEntityClass(), property)) {
					objects[i] = null;
				} else if (!isInsert && !PoEntityHelper.isUpdatable(getEntityClass(), property)) {
					objects[i] = IcwUtils.idObj(objects[i]);
				}
			}
			params.put(property, objects);
		} else {
			if (!cascadable) {
				params.put(property, IcwUtils.idObj(value));
			}
		}
		return false;
	}

	private boolean isExistsMultiPart(Map<String, Object> patchs) {
		return patchs.keySet().stream().anyMatch(property -> Collection.class.isAssignableFrom(BeanUtils.getPropertyDescriptor(getEntityClass(), property).getReadMethod().getReturnType()));
	}

	private boolean isPersisted(Object value) throws Exception {
		return value == null || (PoEntityHelper.readValueByProperty(value, "id") != null && Map.class.cast(value).keySet().size() == 1);
	}

	private boolean isPersisted(Map<String, Object> params, String property) throws Exception {
		Object[] values = readValueAsArray(params, property);
		boolean result = true;
		for (Object value : values) {
			if (!isPersisted(value)) {
				result = false;
				break;
			}
		}
		return result;
	}

	private boolean isInserting(Map<String, Object> params) {
		return params.get("id") == null;
	}

	private boolean isUpdating(Map<String, Object> params) {
		return params.get("id") != null;
	}

	// ------------------------ excel ---------------------------------
	@SuppressWarnings("unchecked")
	@Override
	public void getexcel(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response, Map<String, Object> data) throws Exception {
		Map<String, Object> criteria;
		List<Object> projections;
		if (false || // e
				(criteria = de().parseMap(params.get("criteria"))) == null || // e
				(projections = de().parse(criteria.get("projections"), LinkedList.class)) == null) {
			throw new Exception("criteria.projections does not exists. ");
		} else if (projections.size() < 2) {
			throw new Exception("criteria.projections ");
		} else {
			getlist(params, request, response, data);
			List<Object> list = new LinkedList<>(de().parseCollection(data.get("list")));
			List<Map<String, Object>> ds = new LinkedList<>();
			for (int i = 0; i < list.size(); i++) {
				ds.add(create_ds_item(de().parse(PoEntityHelper.readValueByProperty(list.get(i), "values"), LinkedList.class), projections));
			}
			Collection<ExcelHeader> hs = new LinkedList<>();
			for (int i = 0; i < projections.size(); i++) {
				hs.add(create_hs_item(de().parseMap(projections.get(i))));
			}
			IcwUtils.setFileName(response, getEntityClass().getSimpleName() + ".xlsx");
			new ExcelPort(hs, ds).out(response.getOutputStream());
		}
	}

	private ExcelHeader create_hs_item(Map<String, Object> projection) {
		String property = (String) projection.get("property");
		String fieldName = fetchFeildName(property);
		String fieldTitle = fetchCommentByField(getEntityClass(), property);
		return new ExcelHeader(fieldTitle, fieldName);
	};

	private Map<String, Object> create_ds_item(List<?> values, List<?> projections) throws Exception {
		Map<String, Object> ds_item = new LinkedHashMap<>();
		if (values.size() != projections.size()) {
			throw new Exception("Impossible Exception that projections.length != values.length.");
		} else {
			for (int i = 0; i < projections.size(); i++) {
				Map<String, Object> projection = de().parseMap(projections.get(i));
				String property = (String) projection.get("property");
				String fieldName = fetchFeildName(property);
				ds_item.put(fieldName, values.get(i));
			}
		}
		return ds_item;
	}

	private String fetchFeildName(String property) {
		return property.substring(property.lastIndexOf(".") + 1, property.length());
	}

	private String fetchCommentByField(Class<T> entityClass, String property) {
		return StringUtils.join((property.substring(0, 1).toUpperCase() + property.substring(1)).split("\\."), ' ');
	}

	// ------------------ statics ------------------------------------
	protected static GenericSrvProxy srv() {
		return BeanCtxUtils.getBeanByType(GenericSrvProxy.class);
	}

	protected static DataExchange de() {
		return BeanCtxUtils.getBeanByType(DataExchange.class);
	}
}
